﻿/*
 * Copyright 2009 BAKUDO Michał Rabiński	 
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package bakudo.utils.scrollers.smartScroll {
	import bakudo.utils.massPopulator.MassPopulator;
	import bakudo.utils.scrollers.smartScroll.events.SmartScrollEvent;
	import bakudo.utils.scrollers.smartScroll.globals.ColumnAlign;
	import bakudo.utils.scrollers.smartScroll.globals.RowAlign;
	import bakudo.utils.scrollers.smartScroll.globals.ScrollOrientation;
	import bakudo.utils.scrollers.smartScroll.scrollable.ScrollableItem;
	import bakudo.utils.scrollers.smartScroll.scrollable.ScrollableItemVO;

	import flash.display.GradientType;
	import flash.display.Graphics;
	import flash.display.MovieClip;
	import flash.display.Sprite;
	import flash.events.Event;
	import flash.events.KeyboardEvent;
	import flash.geom.Matrix;
	import flash.geom.Rectangle;
	import flash.utils.getDefinitionByName;
	import flash.utils.getTimer;

	/**
	 * @author BAKUDO Michał Rabiński
	 */
	public class BakudoSmartScroll extends MovieClip {

		//::::::::::::::: public vars ::::::::::::::://
		//gap betwen items
		public var itemsGap : uint = 10;

		//rectangle used to measure masked area
		public var viewRect : Rectangle = new Rectangle(0, 0, 300, 200); 		

		//container for items	
		public var itemsContainer : Sprite = new Sprite();

		//array containing scrollableItemVO's
		public var scrollableVOs : Array = new Array();

		//mask used to mask itemsContainer
		public var itemsMask : MovieClip = new MovieClip();

		/**
		 * custom mask used to mask itemsContainer
		 * it overrides default itemsMask
		*/
		public var customItemsMask : MovieClip;

		//width of single item ( all items has the same width )	
		public var itemWidth : Number;

		//width of single item ( all items has the same width )	
		public var itemHeight : Number;

		//amount of items updated in slide method
		public var maxMoveAmount : uint = 100;

		//max items created in one single loop
		public var maxLoopItems : uint = 500; 

		//orientation of scrolling, can be horizontal or vertical 
		public var scrollOrientation : String = ScrollOrientation.HORIZONTAL;

		//horizontal align of row if horizontal scrollOrientation is set
		public var rowAlign : String = RowAlign.MIDDLE;

		//vertical align of column if vertical scrollOrientation is set
		public var columnAlign : String = ColumnAlign.MIDDLE;

		//::::::::::::::: private vars ::::::::::::::://
		//object passed to ScrollableItem Class constructor
		private var initObject : Object;

		//initial array which elements are passed to newly 
		//created ScrollableItem by item index
		private var initArray : Array;

		//Class name of item that is extending ScrollableItem
		private var itemClassName : String;

		//indicates creation proces end status
		private var _populated : Boolean;

		//current index	
		private var currentIdex : uint;

		//max index of nonExcluded items
		private var maxIndex : uint;

		//used when optimalizing application speed
		private var maxTime : uint;

		public function BakudoSmartScroll() {
			addEventListener(Event.ADDED_TO_STAGE, onATS, false, 0, true);
		}

		//::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
		// PROTECTED METHODS
		//::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
		protected function init() : void {
			if(!itemsContainer.parent)addChild(itemsContainer);
			createMask();
			addListeners();				
		}

		//::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
		// PUBLIC GETTERS
		//::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
		public function get populated() : Boolean {
			return _populated;
		}

		//::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
		// PUBLIC METHODS
		//::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
		/**
		 * Updates items, smoothly or roughly
		 * @param smooth - if true then applies smooth update
		 * @param updateAll - if true, updates all elements, if false updates
		 * only elements within range
		 */
		public function updateView(smooth : Boolean = true,updateAll : Boolean = true) : void {
			update(smooth, updateAll);
		}

		public function setIndexAndScroll(indexToScroll : uint) : void {
			currentIdex = indexToScroll;
			update();
		}

		/**
		 * excludes itemVO from view 
		 * @param	itemIndex - index of excluded item
		 */
		public function excludeItem(itemIndex : uint) : void {
			var scrollableItemVO : ScrollableItemVO;
			var newAxisIndex : uint;
			maxIndex = 0;
			var i : String;
			for (i in scrollableVOs) {
				scrollableItemVO = (scrollableVOs[i] as ScrollableItemVO);
				if (scrollableItemVO.itemIndex == itemIndex) {
					scrollableItemVO.excludeFromList = true;
					scrollableItemVO.update();
				}
				if(!scrollableItemVO.excludeFromList) {
					scrollableItemVO.itemAxisIndex = newAxisIndex;
					newAxisIndex++;
				}
			}
			maxIndex = newAxisIndex;
			updateCurrentIndex();
			update();
		}

		/**
		 * excludes 
		 */
		public function excludeItems(itemsList : Array) : void {
		}

		/**
		 * creates given amount of items of itemClassName, parsing initObject 
		 * to every created objects if initObject is passed to this method
		 * @param	itemClassName - linkage id in library
		 * @param	amount - amout of created items
		 * @param	initObject - initObject passed to item
		 */
		public function populate(itemClassName : String,amount : uint,initParam : * = null) : void {
			
			if(initParam is Object && !initParam is Array) {
				initObject = initParam;	
			}else if(initParam is Array) {
				initArray = initParam;
			}
			
			this.itemClassName = itemClassName;
			//.................................................................
			var ClassName : Class = getDefinitionByName(itemClassName) as Class;
			var item : ScrollableItem = new ClassName as ScrollableItem;
			itemWidth = item.itemWidth;
			itemHeight = item.itemHeight;
			//.................................................................
			var populator : MassPopulator = new MassPopulator();
			populator.amount = amount;
			populator.maxLoopItems = maxLoopItems;
			populator.createItemHandler = createItem;
			populator.allItemsPopulatedHandler = setPopulated;
			populator.populate(itemClassName, amount, initObject);
		}

		/**
		 * scrolls elements by given percent
		 * @param percent - percent from range (0,1)
		 * when percent == maxIndex, maxIndex has been reached
		 * gdy percent == 0, 0 index has been reached
		 */
		public function scrollContentByPercent(percent : Number) : void {
			var maxIndex : uint = scrollableVOs.length;
			currentIdex = Math.floor((maxIndex - 1) * percent);
			update(false, true);
		}

		/**
		 * decrease currentIndex which results
		 * in moving items to the left
		 */
		public function moveLeft() : void {
			if(currentIdex > 0)currentIdex--;
			if(_populated)update();
		}

		/**
		 * increase currentIndex which results
		 * in moving items to the right
		 */
		public function moveRight() : void {
			if(currentIdex < maxIndex)currentIdex++;
			if(_populated)update();
		}

		/**
		 * increase currentIndex which results
		 * in moving items to the bottom
		 */
		public function moveDown() : void {
			if(currentIdex < maxIndex)currentIdex++;
			if(_populated)update();
		}

		public function moveContainer(posx : Number,posy : Number) : void {
			viewRect.x = posx;
			viewRect.y = posy;
			itemsContainer.x = viewRect.x;				
			itemsContainer.y = viewRect.y;
			itemsMask.x = viewRect.x;
			itemsMask.y = viewRect.y;
		}

		//:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
		// PRIVATE METHODS
		//:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
		private function createItem(i : uint) : void {
			var item : ScrollableItem;
			var itemvo : ScrollableItemVO;
			var displayItemClassName : Class = getDefinitionByName(itemClassName) as Class;
			item = new displayItemClassName as ScrollableItem;
			itemvo = new ScrollableItemVO(this);
			if(initObject) {
				itemvo.data = initObject;
			}else if(initArray) {
				itemvo.data = (initArray[i]) as Object;
			}
			scrollableVOs.push(itemvo);
			itemvo.itemIndex = i;			itemvo.itemAxisIndex = i;
			itemvo.scrollableItem = item;
			itemvo.posX = 0;
			itemvo.posY = 0;
			itemvo.handleElementReady();
		};

		private function setPopulated() : void {
			_populated = true;
			maxIndex = scrollableVOs.length;
			update(false, true);
			dispatchEvent(new SmartScrollEvent(SmartScrollEvent.ALL_ITEMS_CREATED, false, false));
		}

		//::TODO 
		// zrobic Align.center i align.distribute, czyli centrowanie wyznaczonego
		//elementu na srodek viewContainer'a oraz w przypadku distribute
		//dokladnie przy krawedzi
	

		/**
		 * slides elements smoothly or roughly depending on passed first parameter
		 * updates elements if their index is greater than (currentIndex - 
		 * maxMoveAmount) and less than (currentIndex + maxMoveAmount)
		 * or updates all elements when updateAll flag is passed to this method
		 * 
		 * @param	smooth - tweens index or simply changes it
		 * @param	updateAll - update items with index distance 
		 * 			up to maxMoveAmount or all items
		 */	
		private function update(smooth : Boolean = true,updateAll : Boolean = false) : void {
			var itemvo : ScrollableItemVO;
			var i : uint;
			var n : uint = scrollableVOs.length;
			var offset : Number;
			var currentIdexOffset : Number;
			var posx : Number;
			var posy : Number;
			var _x : Number = (viewRect.width * .5) - itemWidth * .5;
			var _y : Number = (viewRect.height * .5) - itemHeight * .5;
			var accuracyDistance : uint;
			var time : Number = getTimer();
			while(i < n) {
				itemvo = scrollableVOs[i] as ScrollableItemVO;
				switch(scrollOrientation) {
					case ScrollOrientation.HORIZONTAL:
						offset = (itemWidth + itemsGap) * itemvo.itemAxisIndex;
						currentIdexOffset = (itemWidth + itemsGap) * currentIdex;
						posx = _x + offset - currentIdexOffset;
						switch(rowAlign) {
							case RowAlign.MIDDLE:
								posy = (viewRect.height * .5) - itemHeight * .5;
								break;
						}
						break;
					case ScrollOrientation.VERTICAL:
						offset = (itemHeight + itemsGap) * itemvo.itemAxisIndex;
						currentIdexOffset = (itemHeight + itemsGap) * currentIdex;
						posy = _y + offset - currentIdexOffset;
						switch(columnAlign) {
							case ColumnAlign.MIDDLE:
								posx = (viewRect.width * .5) - itemWidth * .5;
								break;
						}
						break;	
				}
				
				accuracyDistance = Math.abs(itemvo.itemAxisIndex - currentIdex);
				if(accuracyDistance < maxMoveAmount || updateAll) {
					itemvo.changePosition(posx, posy, smooth);
				} else {
					itemvo.update(); 
				}
				i++;
			}
			if((getTimer() - time) > maxTime)maxTime = (getTimer() - time);
			//trace('maxTimeAmount ', maxTime);
		}

		private function addListeners() : void {
			stage.addEventListener(KeyboardEvent.KEY_DOWN, onKDown, false, 0, true);
		}

		/**
		 * creates default mask applied to items container
		 */	
		private function createMask() : void {
			if(!customItemsMask) {
				var g : Graphics = itemsMask.graphics;
				var m : Matrix = new Matrix();
				var degrees : Number = 0;
				var radians : Number = degrees * Math.PI / 180;
				var colors : Array = [0x000000,0xFFFFFF,0xFFFFFF,0xFFFFFF];
				var alphas : Array = [0,1,1,0];
				var ratios : Array = [0,63.75,177.5,255] ;
				m.createGradientBox(viewRect.width, viewRect.height, radians);  
				g.beginGradientFill(GradientType.LINEAR, colors, alphas, ratios, m);
				g.drawRect(0, 0, viewRect.width, viewRect.height);
				g.endFill();
				addChild(itemsMask);
			}
			itemsMask = customItemsMask ? customItemsMask : itemsMask;
			itemsMask.x = viewRect.x;
			itemsMask.y = viewRect.y;
			itemsMask.cacheAsBitmap = true;
			itemsContainer.cacheAsBitmap = true;
			itemsContainer.mask = itemsMask;
			showViewBoundary();
		}

		private function showViewBoundary() : void {
			var g : Graphics = graphics;
			g.lineStyle(1, 0xFF0000, .3);
			g.drawRect(viewRect.x, viewRect.y, viewRect.width, viewRect.height);
		}

		private function updateCurrentIndex() : void {
			if(currentIdex < 0)currentIdex = 0;
			if(currentIdex > maxIndex)currentIdex = maxIndex;
		}

		//::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
		// PRIVATE LISTENERS
		//::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
		private function onATS(event : Event) : void {
			init();
		}

		private function onKDown(event : KeyboardEvent) : void {
			switch(event.keyCode) {
				case 37:
					moveLeft();
					break;
				case 38:
					moveLeft();
					break;
				case 39:
					moveRight();
					break;
				case 40:
					moveRight();
					break;
			}
		}
	}
}
